Add vLLM offline backend with micro-batching support - #736
Conversation
fc01371 to
bbe2874
Compare
efa1d9e to
942fa2e
Compare
942fa2e to
5d2304d
Compare
|
Hi @maryamtahhan, the DCO check has failed. Please click on DCO in the Checks section for instructions on how to resolve this. |
664810c to
251eb67
Compare
|
@sjmonson @jaredoconnell this PR has been rebased and is green again |
dbutenhof
left a comment
There was a problem hiding this comment.
I think the code is mostly OK (except it should be updated to match the current backend init conventions). The documentation needs to be updated to 0.7 CLI conventions.
c581c3c to
5ee9091
Compare
dbutenhof
left a comment
There was a problem hiding this comment.
Some comments ... the ones around shutdown are actually from a Cursor AI review of your branch -- there's more, but I didn't copy everything.
Thank you @dbutenhof I will have a look |
5c21919 to
69eceea
Compare
|
not ready for re-review... will add a comment when it is. Still doing some testing |
|
Ready for Re-review |
| # Hide the inherited ``stream`` field -- offline is never streaming. | ||
| stream: Literal[False] = Field( # type: ignore[assignment] | ||
| default=False, | ||
| exclude=True, | ||
| description="Offline backend does not support streaming.", | ||
| ) |
There was a problem hiding this comment.
It would be cleaner to build a common base class for the in-process vLLM backends (possibly in common.py) and then add stream only to the asynchronous subclass rather than trying to hide it here. I guess I won't quite go to the extent of demanding a change here, since it is more work & touch surface and this will probably suffice ... but it would be cleaner. 😁
jaredoconnell
left a comment
There was a problem hiding this comment.
Looks pretty good. I have one comment.
Introduces VLLMOfflineBackend inheriting from VLLMPythonBackend, using vLLM's synchronous LLM engine for micro-batched inference. Wires RequestStateStats metrics into response timings. Includes registration test, docs, and backends.md update. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Use llm.get_tokenizer() instead of llm.llm_engine.tokenizer.tokenizer which doesn't exist in the V1 engine. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
- Apply ruff format to offline.py - Fix _wire_vllm_metrics: only use token counts, not monotonic timestamps that produce garbage when mixed with wall-clock times - Apply mdformat to markdown docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
The vLLM LLM engine is now created lazily on the first resolve() call instead of during process_startup(). This avoids a double-startup that wastes resources reloading model weights and can cause CPU-affinity degradation when the engine is created in both the validation path and the worker process. Uses double-checked locking with asyncio.Lock to prevent concurrent engine creation when multiple requests arrive simultaneously. Adds _reset_cpu_affinity() to restore the full cgroup cpuset before engine creation, working around OpenMP/torch restricting CPU affinity in forked worker processes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Map vLLM's monotonic RequestStateStats timestamps to wall-clock values so TTFT, ITL, and TPOT are reported for offline batches. Split _process_batch into _take_pending_batch + _run_generate to release the batch lock during LLM.generate(), and loop the deferred flush until all pending requests are drained. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
…n, call shutdown - Move _reset_cpu_affinity to common.py so both vllm_python backends can reuse it; add comment explaining cgroup v2/v1 fallback loop - Guard resolve() with _shutting_down check to reject requests during shutdown - Call llm.shutdown() before deleting the engine in process_shutdown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
- Add _generate_lock to serialize LLM.generate() calls, preventing overlapping generates on the non-thread-safe sync LLM engine - Move _shutting_down check + enqueue under the same batch lock acquisition so shutdown cannot strand in-flight resolve() waiters - Set _shutting_down under lock in process_shutdown for atomicity - Remove unused _BatchedRequest fields (request, request_info, request_id) and the uuid import - Drop duplicate validate_vllm_config; inherited from parent Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Covers engine laziness, lifecycle/shutdown, batch processing, deferred flush, generate-lock serialization, shutdown-guard, and metrics wiring with 31 test cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Shutdown now waits for the deferred-flush task to complete and acquires _generate_lock before tearing down the engine, preventing a race where an in-flight LLM.generate() outlives _llm teardown. Add batch_timeout field (default 0.01s) to VLLMOfflineBackendArgs so partial batches accumulate for a configurable window before flushing; full batches still flush immediately. Add ## WRITTEN BY AI ## docstrings to all 34 test functions, replace the old cancellation test with test_shutdown_waits_for_inflight_generate, and add batch_timeout validation tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
… metrics Use throughput,max_concurrency=20 instead of constant,rate=3 in the offline backend guide example to better reflect the backend's throughput-oriented purpose. Replace all getattr calls in _wire_vllm_metrics with hasattr + direct attribute access per AGENTS.md coding standards. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
The benchmark framework calls process_startup/validate/shutdown in the parent process, then forks worker subprocesses that call process_startup again. Asyncio locks created on the parent event loop are unusable in the child, causing the worker to hang and produce 0 completed requests. Recreate all asyncio primitives (_batch_lock, _generate_lock, _engine_lock) and reset batch state in process_startup() so each worker gets fresh locks bound to its own event loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
The vLLM engine cold-start (42s on CPU) was happening during the timed benchmark phase inside resolve(), causing SIGTERM to kill the EngineCore before any requests could complete. Record the creator PID in __init__ so validate() can detect forked workers and eagerly preload the engine before the timed phase. Clear _llm in process_startup so workers never inherit a stale engine handle from the parent process. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Rewrite the Engine lifecycle section to clearly describe the three-stage flow: parent preflight (cheap check), worker preload (PID-based eager engine creation), and resolve() idempotent fallback. Mention both fork and spawn workers. Strengthen test_startup_recreates_locks: use `is not` per lock, assert batch state is reset, and acquire/release each new lock to prove they work on the current event loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
- Replace hasattr guards in _wire_vllm_metrics with direct field access on vllm.RequestOutput / RequestStateStats; type request_output properly and cast at the call site in resolve() - Add explanatory comment in reset_cpu_affinity() for the cgroup v2/v1 loop-with-return pattern that reads as odd at first glance - Call reset_cpu_affinity() in VLLMPythonBackend.process_startup() so the regular in-process vLLM backend benefits from the same CPU-affinity fix as the offline backend - Replace 'resolve() fallback' with 'Inference-time safety net' in docs to avoid exposing the internal method name to end users Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
681c155 to
2426cb1
Compare
Use getattr with 0.0 defaults for timing fields so that metrics objects carrying only num_generation_tokens (e.g. test stubs) no longer raise AttributeError after the metrics-is-None guard. Also update the validate() docstring to say "inference-time safety net", fix the test_no_metrics_no_crash docstring, and add a smoke test asserting reset_cpu_affinity() is called in VLLMPythonBackend.process_startup(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
f700306 to
8b4c560
Compare
Replace fragile _creator_pid comparison with multiprocessing.parent_process() so scheduler workers reliably preload the vLLM engine before the timed phase, avoiding 0-completion runs when cold-start lands inside resolve(). Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Rich Live's background auto-refresh thread re-rendered the Benchmarks panel
every 250 ms. Between renders, vLLM worker processes wrote to the inherited
terminal file descriptors during engine initialisation, shifting the cursor
so each re-render landed below the previous one instead of overwriting it.
Switch to manual refresh (auto_refresh=False) and drive repaints explicitly:
* _force_refresh() — immediate render on state-transition events
(benchmark start, complete, finalize)
* _throttled_refresh() — caps update rate at refresh_per_second (~4 fps)
during active benchmarking; no-op between events
The window between on_benchmark_start() and the first on_benchmark_update()
now contains zero renders, so worker-process writes cannot cause stacking.
entrypoints.py calls prepare_vllm_benchmark_logging() before workers start
so env-var silencing is inherited by child processes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Worker processes write HF Hub progress bars, tqdm model-loading bars, and C-level PyTorch warnings (e.g. NUMA) directly to the inherited terminal file descriptors during engine initialisation. These writes interleave with the main-process Rich live panel even when Python-level log levels are lowered. Fixes: * Add suppress_worker_stdio() (utils/terminal.py) — redirects OS-level fd 1/fd 2 via dup2() to /dev/null and replaces sys.stdout/sys.stderr, silencing both C-level and Python-level writes for the duration. * Wrap backend.validate() in worker._processing_startup() with suppress_worker_stdio() so the vLLM LLM engine cold-start is silent. * Add VLLM_CONFIGURE_LOGGING=0 to prepare_vllm_benchmark_logging() so vLLM's EngineCore subprocesses do not re-run their own log handler setup after the level has been lowered. * Call prepare_vllm_benchmark_logging() in VLLMPythonBackend.process_startup() and use vllm_benchmark_engine_config() for the async engine so the online backend also benefits from quieter defaults. * Fix offline batch waiter hang: replace zip(..., strict=True) outside the try/except in _run_generate() with an explicit length check that routes mismatches through the error path, ensuring all batch waiters always receive ready.set() and resolve() calls never hang. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
1a2ed7a to
d115583
Compare
* terminal.py: store both open(os.devnull) handles before the try block and close them in finally, eliminating the two-fd leak per call. * test_progress.py: add ## WRITTEN BY AI ## module docstring, smoke/sanity markers, and descriptive per-test docstrings per AGENTS.md; extract the repeated patch target into a module-level constant to stay within the 79-char line limit. * test_vllm_offline.py: add test_run_generate_output_count_mismatch_signals _all_waiters — verifies the len(outputs) != len(batch) path routes all waiters through the error handler so resolve() never hangs. * test_terminal.py: new POSIX unit test file for suppress_worker_stdio() covering stdout/stderr silencing, sys.stdout/stderr restoration, fd restoration, fd-leak detection, and exception-safety. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Defer asyncio lock creation to process_startup() and strip locks in pickle state so spawn workers can unpickle the backend. Route all _run_generate() failures through _signal_batch_failure() so every batch waiter is unblocked on any exception. Co-authored-by: Cursor <cursoragent@cursor.com> Signed-off-by: Maryam Tahhan <mtahhan@redhat.com>
Add vLLM Offline Backend for Batch Processing
Adds
VLLMOfflineBackendfor batch-oriented inference using vLLM's synchronousLLMengine. Requests are queued and dispatched in configurable micro-batches viaLLM.generate(), removing per-request scheduling overhead. Ideal for throughput benchmarking.Summary
VLLMOfflineBackendextendingVLLMPythonBackendwith batch processingvalidate(): workers eagerly create the engine before the timed phase; the parent process never loads model weightsprocess_startup()so asyncio primitives work after forkRequestStateStatsfor TTFT/ITL/TPOT reportingcommon.pymodule for utilities used by both vLLM backendsArchitecture
VLLMOfflineBackendinherits fromVLLMPythonBackend, reusing chat template resolution, multimodal data handling, request formatting, and sampling parameter creation. Only the engine lifecycle and request processing are overridden.Key Design Decisions
_creator_pidis recorded in__init__;validate()detects forked/spawned workers (PID mismatch) and calls_ensure_engine()to preload the engine before the timed benchmark phase. The parent preflight skips engine creation entirely.resolve()still calls_ensure_engine()as an idempotent fallback.process_startup()recreates all asyncio locks, clears_llm, and resets batch state — locks from the parent's event loop are unusable in the child._ensure_engine()uses anasyncio.Lockso concurrent callers create only one engine._take_pending_batch()snapshots the queue under lock,_run_generate()runs lock-free so new requests can enqueue during generation._generate_lockserializesLLM.generate()calls (vLLM's sync LLM is not safe for overlapping generates)._shutting_downcheck and enqueue happen under the same_batch_lockacquisition. Shutdown waits for in-flight generates and acquires_generate_lockbefore teardown.batch_timeout(default 0.01s) controls how long partial batches wait before flushing; full batches bypass the delay._deferred_flushloops until_pending_batchis empty, preventing requests stuck when new ones arrive during generate.Files Changed
src/guidellm/backends/vllm_python/offline.pysrc/guidellm/backends/vllm_python/common.pyreset_cpu_affinity()utilitysrc/guidellm/backends/vllm_python/__init__.pyVLLMOfflineBackenddocs/guides/vllm-offline-backend.mddocs/guides/backends.mdtests/unit/backends/vllm_python/test_vllm_offline.pyUsage
Test Plan
Unit Tests (39 passing)
_ensure_enginecreates once, concurrent callers get single enginellm.shutdown(), drains pending batch, awaits deferred flush task, acquires_generate_lockbefore teardownis notand acquire/release),_llmcleared, batch state resetvalidate()skips engine in parent, preloads in worker (simulated PID mismatch),_creator_pidset in__init___take_pending_batchclears queue,_run_generatedistributes results, error signals all waiters, multimodal prompt format_run_generatecallsresolve()rejects during shutdown, flag set under locknum_generation_tokenswith fallback, timing fromRequestStateStats,queued_tsfallbackEC2 Integration Test (verified)
Benchmarked on EC2 with
facebook/opt-125m(CPU):RequestStateStatsVanilla vLLM Comparison (verified)
Ran 10 identical prompts through vanilla
vllm.LLM.generate()and GuideLLM's offline backend:Use of AI
git log
commit 2257608
Author: Maryam Tahhan mtahhan@redhat.com
Date: Fri Jul 17 14:30:31 2026 +0100
commit ba439f6
Author: Maryam Tahhan mtahhan@redhat.com
Date: Fri Jul 17 15:33:26 2026 +0100
commit 5467ec7
Author: Maryam Tahhan mtahhan@redhat.com
Date: Fri Jul 17 15:41:25 2026 +0100
commit 656f1d1
Author: Maryam Tahhan mtahhan@redhat.com
Date: Mon Jul 20 17:27:48 2026 +0100
commit 296f255
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Jul 21 12:57:05 2026 +0100
commit 95d3d21
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Jul 21 14:31:04 2026 +0100
commit f923ade
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Jul 21 15:55:21 2026 +0100
commit 2bf2784
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Jul 21 16:11:06 2026 +0100
commit 7fe1f04
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Jul 21 16:26:23 2026 +0100
commit 0ac49c3
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Jul 21 16:41:47 2026 +0100
commit ae86bad
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Jul 21 17:33:45 2026 +0100
commit 4bc70b7
Author: Maryam Tahhan mtahhan@redhat.com
Date: Wed Jul 22 10:31:21 2026 +0100
commit cd16ce5
Author: Maryam Tahhan mtahhan@redhat.com
Date: Wed Jul 22 10:41:26 2026 +0100
commit 2426cb1
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Aug 4 10:38:30 2026 +0100
commit 8b4c560
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Aug 4 10:44:04 2026 +0100
commit 8cfe262
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Aug 4 11:21:35 2026 +0100
commit 84603fa
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Aug 4 14:22:05 2026 +0100
commit d115583
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Aug 4 14:22:24 2026 +0100
commit fb65835
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Aug 4 14:30:12 2026 +0100
commit 4fb1368
Author: Maryam Tahhan mtahhan@redhat.com
Date: Tue Aug 4 15:20:28 2026 +0100
Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Co-authored-by: Cursor cursoragent@cursor.com
Signed-off-by: Maryam Tahhan mtahhan@redhat.com